import os
import sys
import time
import subprocess
from stat import ST_MTIME
from hashlib import md5
from mediadart import log
from mediadart.lock import locking, LOCK_EX, LOCK_NB, LockException
from mediadart.config import Configurator
from twisted.internet import reactor, defer
from twisted.web import resource, server, error

post_headers = [
   'content-length',
   'total_chunks',
   'res_id',
   'chunk_number',
   'md5',
]

CLEANUP_TIMEOUT=1800

class DownloadChunks:
    def __init__(self, headers, options, read_fn):
        self.read_fn = read_fn
        self.content_length = int(headers['content-length'])
        self.md5 = headers['md5']
        self.total_chunks = int(headers['total_chunks'])
        self.chunk_number = int(headers['chunk_number'])
        self.res_id = headers['res_id']
        self.importdir = options['import_dir']
        self.tmpdir = options['temp_dir']
        self.workdir = os.path.join(self.tmpdir, '%s-down' % self.res_id)
        self.tempchunk = os.path.join(self.tmpdir, 'temp-%d-%s.%s' % (os.getpid(), self.res_id, self.chunk_number))
        self.finalchunk = os.path.join(self.workdir, '%s.%06d' % (self.res_id, self.chunk_number))
        self.tempfile = os.path.join(self.importdir, 'temp-%d-%s' % (os.getpid(), self.res_id))
        self.finalfile = os.path.join(self.importdir, self.res_id)
        self.lockfile = os.path.join(self.importdir, 'lock-%s' % self.res_id)
        log.debug('received chunk %d' % self.chunk_number)

    def sanitize_id(self, id):
        """check that id is of the form <hex number>.<ext> and <ext> does not contain os.sep"""
        if not id:
            return 1
        try:
            parts = id.split('.')
        except:
            return 1
        if len(parts) > 2:
            return 1
        try:
            long(parts[0], 16)
        except:
            return 1
        if len(parts) == 2 and parts[1].find(os.sep) >= 0:
            return 1
        return 0

    def check_working_dir(self):
        try:
            os.mkdir(self.workdir)
        except OSError, e:
            if e.errno == 17:  # already there
                pass
            else:
                return 1
        return 0

    def check_file(self, name):
        " returns  1 if name exists"
        try:
            os.stat(name)
            return 1
        except OSError:
            return 0

    def merge_chunks(self, chunks):
        try:
            with locking(open(self.lockfile, 'w'), LOCK_EX|LOCK_NB):
                if self.check_file(self.finalfile):
                    return
                with open(self.tempfile, 'w') as f:
                    for c in chunks:
                        f.write(open(os.path.join(self.workdir, c), 'r').read())
                log.debug('writing file %s' % self.finalfile)
                os.rename(self.tempfile, self.finalfile)
        except LockException:
            pass

    def check_done(self):
        chunks = [x for x in os.listdir(self.workdir) if x.startswith('%s.' % self.res_id)]
        if len(chunks) == self.total_chunks:
            chunks.sort()
            self.merge_chunks(chunks)

    def run(self):
        if self.sanitize_id(self.res_id):
            log.error('invalid id')
            return 1
        if self.check_working_dir():
            log.error('unable to create directory %s' % self.workdir)
            return 1
        if self.check_file(self.finalfile):
            return 0
        if not self.check_file(self.finalchunk):
            data = self.read_fn(self.content_length)
            hash = md5(data).hexdigest()
            if hash != self.md5:
                log.error('hash mismatch')
                return 1
            log.debug('writing chunk')
            with open(self.tempchunk, 'w') as f:
                f.write(data)
            os.rename(self.tempchunk, self.finalchunk)
        self.check_done()
        return 0

class UploadServer(resource.Resource):
    isLeaf = True
    _conf = Configurator()
    _downdir = _conf.get('storage', 'import_dir')
    _tmpdir = _conf.get('storage', 'temp_dir')

    def __init__(self):
        resource.Resource.__init__(self)
        self.__clean_up()

    def __clean_up(self):
        reactor.callLater(CLEANUP_TIMEOUT, self.__clean_up)
        log.debug('scheduled cleanup')
        to_be_removed = []
        now = time.time()
        files = [os.path.join(self._tmpdir, x) for x in os.listdir(self._tmpdir)]
        files += [os.path.join(self._downdir, x) for x in os.listdir(self._downdir)]
        for f in files:
            last_access = os.stat(f)[ST_MTIME]
            if now - last_access > CLEANUP_TIMEOUT:
                #log.debug('removing %s' % f)
                to_be_removed.append(f)
            else:
                #log.debug('keeping %s' % f)
                pass
        subprocess.Popen(['/bin/rm', '-rf'] + to_be_removed)

    def render_POST(self, request):
        request.setResponseCode(200, "bello")
        headers = dict([(x, request.getHeader(x)) for x in post_headers])
        options = {'import_dir': self._downdir, 'temp_dir':self._tmpdir}
        readfn = request.content.read
        if DownloadChunks(headers, options, readfn).run() == 0:
            code, msg = 200, "Ok"
        else:
            code, msg = 400, "Bad request"
        request.setResponseCode(code, msg)
        request.setHeader('Content-Type', 'text/plain')
        return '%s\r\n' % headers

def start_server(root):
    root.putChild('uploader', UploadServer())

if __name__ == '__main__':
    if len(sys.argv)==1:
        port = 8081
    else:
        port = int(sys.argv[1])
    log.warning('starting upload server on port %d' % port)
    r = resource.Resource()
    reactor.listenTCP(int(port), server.Site(r))
    start_server(r)
    reactor.run()
